Passed
Push — master ( c0399f...7baacb )
by Tony
01:53
created

Always.attach   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
c 0
b 0
f 0
nc 2
dl 0
loc 7
rs 9.4285
nop 1
1
(function (jQuery) {
2
'use strict';
3
4
jQuery = jQuery && jQuery.hasOwnProperty('default') ? jQuery['default'] : jQuery;
5
6
var AlwaysData = /** @class */ (function () {
7
    function AlwaysData() {
8
        this.instance = null;
9
        this.lastOperation = null;
10
    }
11
    AlwaysData.OPERATION_INSERTION = 0;
12
    AlwaysData.OPERATION_REMOVAL = 1;
13
    return AlwaysData;
14
}());
15
16
var Always = /** @class */ (function () {
17
    /**
18
     * Constructor.
19
     *
20
     * @param {HTMLElement} element
21
     */
22
    function Always(element) {
23
        var _this = this;
24
        this.insertedCallbacks = {};
25
        this.removedCallbacks = {};
26
        this.element = element;
27
        this.observer = new MutationObserver(function (mutations) { return mutations.forEach(function (mutation) {
28
            if ('childList' !== mutation.type) {
29
                return;
30
            }
31
            // NodeList does not support forEach directly due to a bug in Google Chrome
32
            [].forEach.call(mutation.addedNodes, function (node) {
33
                if (!(node instanceof HTMLElement)) {
34
                    return;
35
                }
36
                _this.notifyInserted(node);
37
            });
38
            [].forEach.call(mutation.removedNodes, function (node) {
39
                if (!(node instanceof HTMLElement)) {
40
                    return;
41
                }
42
                _this.notifyRemoved(node);
43
            });
44
        }); });
45
        this.observer.observe(this.element, {
46
            childList: true,
47
            subtree: true
48
        });
49
    }
50
    /**
51
     * Retrieve a jQuery Always specific data object assigned to the specified element, or create one if it does
52
     * not already exist.
53
     *
54
     * @param {HTMLElement} element
55
     * @returns {AlwaysData}
56
     */
57
    Always.data = function (element) {
58
        if (!element.hasOwnProperty('jQueryAlways')) {
59
            Object.defineProperty(element, 'jQueryAlways', {
60
                value: new AlwaysData(),
61
                configurable: true
62
            });
63
        }
64
        return element.jQueryAlways;
65
    };
66
    /**
67
     * Attaches a new Always instance to the specified element and returns it, or returns a previously attached
68
     * instance.
69
     *
70
     * @param {HTMLElement} element
71
     * @returns {Always}
72
     */
73
    Always.attach = function (element) {
74
        var data = this.data(element);
75
        if (!data.instance) {
76
            data.instance = new Always(element);
77
        }
78
        return data.instance;
79
    };
80
    /**
81
     * Detaches a previously attached Always instance from the specified element & also removes the mutation
82
     * observer.
83
     *
84
     * @param {HTMLElement} element
85
     */
86
    Always.detach = function (element) {
87
        Always.attach(element).observer.disconnect();
88
        delete element.jQueryAlways;
89
    };
90
    /**
91
     * Normalizes similar selectors.
92
     * E.g. "a, b" and "b,a" are the same thing, both will be normalized to "a,b".
93
     *
94
     * @param {string} selector
95
     * @returns {string}
96
     */
97
    Always.normalizeSelector = function (selector) {
98
        return selector.split(',').map(function (part) {
99
            return part.trim();
100
        }).sort().join(',');
101
    };
102
    /**
103
     * Attaches the specified inserted / removed listeners for elements matching the specified selector on the
104
     * specified parent element.
105
     *
106
     * @param {HTMLElement} element
107
     * @param {string} selector
108
     * @param {() => void} onInserted
109
     * @param {() => void} onRemoved
110
     */
111
    Always.always = function (element, selector, onInserted, onRemoved) {
112
        var instance = Always.attach(element);
113
        // register inserted callbacks
114
        if ('function' === typeof onInserted) {
115
            instance.addInsertedCallback(selector, onInserted);
116
            [].forEach.call(element.querySelectorAll(selector), function (node) {
117
                onInserted.call(node);
118
            });
119
        }
120
        // register removed callbacks
121
        if ('function' === typeof onRemoved) {
122
            instance.addRemovedCallback(selector, onRemoved);
123
        }
124
    };
125
    /**
126
     * Detaches the specified inserted / removed listener(s) for elements matching the specified selector on the
127
     * specified element as parent.
128
     *
129
     * @param {HTMLElement} element
130
     * @param {string} selector
131
     * @param {() => void} onInserted
132
     * @param {() => void} onRemoved
133
     */
134
    Always.never = function (element, selector, onInserted, onRemoved) {
135
        // if no selector is specified, quickest way to remove all listeners is to just detach
136
        if (!selector) {
137
            Always.detach(element);
138
            return;
139
        }
140
        var instance = Always.attach(element);
141
        // if no specific callback is requested, remove all listeners that match the selector
142
        if (!onInserted && !onRemoved) {
143
            instance.removeInsertedCallback(selector);
144
            instance.removeRemovedCallback(selector);
145
            return;
146
        }
147
        // remove only specific listeners
148
        if (onInserted) {
149
            instance.removeInsertedCallback(selector, onInserted);
150
        }
151
        if (onRemoved) {
152
            instance.removeRemovedCallback(selector, onRemoved);
153
        }
154
    };
155
    /**
156
     * Adds a new callback for the specified selector.
157
     *
158
     * @param {{[p: string]: (() => void)[]}} callbacks
159
     * @param {string} selector
160
     * @param {() => void} callback
161
     * @returns {Always}
162
     */
163
    Always.prototype.addCallback = function (callbacks, selector, callback) {
164
        selector = Always.normalizeSelector(selector);
165
        if (!callbacks.hasOwnProperty(selector)) {
166
            callbacks[selector] = [];
167
        }
168
        callbacks[selector].push(callback);
169
        return this;
170
    };
171
    /**
172
     * Removes the specified callback for the specified selector.
173
     * If no callback is specified, removes all callbacks for the selector.
174
     *
175
     * @param {{[p: string]: (() => void)[]}} callbacks
176
     * @param {string} selector
177
     * @param {() => void} callback
178
     * @returns {Always}
179
     */
180
    Always.prototype.removeCallback = function (callbacks, selector, callback) {
181
        selector = Always.normalizeSelector(selector);
182
        if (!callbacks.hasOwnProperty(selector)) {
183
            return this;
184
        }
185
        if (callback) {
186
            for (var index = -1; -1 < (index = callbacks[selector].indexOf(callback));) {
0 ignored issues
show
Unused Code introduced by
The assignment to variable index seems to be never used. Consider removing it.
Loading history...
187
                callbacks[selector].splice(index, 1);
188
            }
189
        }
190
        else {
191
            delete callbacks[selector];
192
        }
193
        return this;
194
    };
195
    /**
196
     * Notifies the specified callbacks for the specified operation on the specified element.
197
     * This is a convenience method for notifyInserted and notifyRemoved.
198
     *
199
     * @param {HTMLElement} element
200
     * @param {{[p: string]: (() => void)[]}} callbacks
201
     * @param {number} operation
202
     * @returns {Always}
203
     */
204
    Always.prototype.notifyCallbacks = function (element, callbacks, operation) {
205
        // make the element manageable & prevent duplicate invocations
206
        // even making every single node in the Dom manageable is still much faster that matching it against a selector
207
        var data = Always.data(element);
208
        if (operation === data.lastOperation) {
209
            return this;
210
        }
211
        data.lastOperation = operation;
212
        // traverse requested callbacks & invoke those for which the element matches the corresponding selector
213
        Object.keys(callbacks).forEach(function (selector) {
214
            if (element.matches(selector)) {
215
                callbacks[selector].forEach(function (callback) {
216
                    callback.call(element);
217
                });
218
            }
219
        });
220
        return this;
221
    };
222
    /**
223
     * Notifies all registered callbacks about an insertion, if the corresponding selector matches the node.
224
     *
225
     * @param {HTMLElement} element
226
     * @returns {Always}
227
     */
228
    Always.prototype.notifyInserted = function (element) {
229
        var _this = this;
230
        // callbacks for insertions are invoked for parents first
231
        this.notifyCallbacks(element, this.insertedCallbacks, AlwaysData.OPERATION_INSERTION);
232
        // we need to manually cascade notify all child nodes as the observer won't do it automatically
233
        [].forEach.call(element.children, function (node) {
234
            _this.notifyInserted(node);
235
        });
236
        return this;
237
    };
238
    /**
239
     * Notifies all registered callbacks about a removal, if the corresponding selector matches the node.
240
     *
241
     * @param {HTMLElement} element
242
     * @returns {Always}
243
     */
244
    Always.prototype.notifyRemoved = function (element) {
245
        var _this = this;
246
        // we need to manually cascade notify all child nodes as the observer won't do it automatically
247
        [].forEach.call(element.children, function (node) {
248
            _this.notifyRemoved(node);
249
        });
250
        // callbacks for removals are invoked for deepest children first
251
        this.notifyCallbacks(element, this.removedCallbacks, AlwaysData.OPERATION_REMOVAL);
252
        return this;
253
    };
254
    /**
255
     * Adds a new inserted callback for the specified selector.
256
     *
257
     * @param {string} selector
258
     * @param {() => void} callback
259
     * @returns {Always}
260
     */
261
    Always.prototype.addInsertedCallback = function (selector, callback) {
262
        return this.addCallback(this.insertedCallbacks, selector, callback);
263
    };
264
    /**
265
     * Adds a new removed callback for the specified selector.
266
     *
267
     * @param {string} selector
268
     * @param {() => void} callback
269
     * @returns {Always}
270
     */
271
    Always.prototype.addRemovedCallback = function (selector, callback) {
272
        return this.addCallback(this.removedCallbacks, selector, callback);
273
    };
274
    /**
275
     * Removes the specified inserted callback for the specified selector.
276
     * If no callback is specified, removes all callbacks for the selector.
277
     *
278
     * @param {string} selector
279
     * @param {() => void} callback
280
     * @returns {Always}
281
     */
282
    Always.prototype.removeInsertedCallback = function (selector, callback) {
283
        return this.removeCallback(this.insertedCallbacks, selector, callback);
284
    };
285
    /**
286
     * Removes the specified removed callback for the specified selector.
287
     * If no callback is specified, removes all callbacks for the selector.
288
     *
289
     * @param {string} selector
290
     * @param {() => void} callback
291
     * @returns {Always}
292
     */
293
    Always.prototype.removeRemovedCallback = function (selector, callback) {
294
        return this.removeCallback(this.removedCallbacks, selector, callback);
295
    };
296
    return Always;
297
}());
298
299
function bindingNative() {
300
    window.Always = {
301
        always: Always.always,
302
        never: Always.never
303
    };
304
}
305
306
function bindingJQuery() {
307
    if ('undefined' === typeof jQuery) {
308
        return;
309
    }
310
    (function ($) {
311
        $.extend($.fn, {
312
            always: function (selector, onInserted, onRemoved) {
313
                return $(this).each(function () {
314
                    Always.always(this, selector, onInserted, onRemoved);
315
                });
316
            },
317
            never: function (selector, onInserted, onRemoved) {
318
                return $(this).each(function () {
319
                    Always.never(this, selector, onInserted, onRemoved);
320
                });
321
            }
322
        });
323
    })(jQuery);
324
}
325
326
var Bindings = /** @class */ (function () {
327
    function Bindings() {
328
    }
329
    Bindings.native = bindingNative;
330
    Bindings.jQuery = bindingJQuery;
331
    return Bindings;
332
}());
333
334
function polyfillMutationObserver() {
335
    if (window.MutationObserver) {
336
        return;
337
    }
338
    window.MutationObserver = window.WebKitMutationObserver;
339
}
340
341
function polyfillElementMatches() {
342
    var _this = this;
343
    var prototype = Element.prototype;
344
    if (prototype.matches) {
345
        return;
346
    }
347
    prototype.matches =
348
        prototype.matchesSelector ||
349
            prototype.mozMatchesSelector ||
350
            prototype.msMatchesSelector ||
351
            prototype.oMatchesSelector ||
352
            prototype.webkitMatchesSelector ||
353
            (function (s) {
354
                var matches = (_this.document || _this.ownerDocument).querySelectorAll(s);
355
                for (var i = 0; i < matches.length; i++) {
356
                    if (matches.item(i) === _this) {
357
                        return true;
358
                    }
359
                }
360
                return false;
361
            });
362
}
363
364
var Polyfills = /** @class */ (function () {
365
    function Polyfills() {
366
    }
367
    Polyfills.elementMatches = polyfillElementMatches;
368
    Polyfills.mutationObserver = polyfillMutationObserver;
369
    return Polyfills;
370
}());
371
372
/** global: Element */
373
/** global: HTMLElement */
374
/** global: MutationObserver */
375
/** global: WebKitMutationObserver */
376
// polyfills
377
Polyfills.elementMatches();
378
Polyfills.mutationObserver();
379
// bindings
380
Bindings.native();
381
Bindings.jQuery();
382
383
}(jQuery));
384